//-------------------------------------------------------------------------------------------------
// Copyright (c) Bradford W. Mott and Flare Contributors
// North Carolina State University, Department of Computer Science
// The IntelliMedia Group
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
// OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
// SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
// OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//-------------------------------------------------------------------------------------------------

using UnityEngine;

namespace Flare.Geom
{
    /// <summary>
    /// A transformation matrix that can perform arbitrary affine transformations mapping
    /// points from one coordinate space to another. Affine transformation preserves lines
    /// and parallelism. The transformation matrix is a 3x3 matrix using homogenous
    /// coordinates with the following elements:
    /// 
    /// \f$\begin{bmatrix} 
    ///     a  &  c  &  t_x    \\ 
    ///     b  &  d  &  t_y    \\ 
    ///     0  &  0  &  1
    /// \end{bmatrix}\f$
    /// 
    /// Various 2D graphical transformations can be performed on a display object using
    /// a transformation matrix, including translation, rotation, scaling, and skewing.
    /// The values of a, b, c, d, tx, and ty can be accessed through the corresponding
    /// properties.
    /// </summary>
    public class Matrix
    {
        /// <summary>
        /// Accesses the 'a' element of the matrix.
        /// </summary>
        public float a { get; set; }

        /// <summary>
        /// Accesses the 'b' element of the matrix.
        /// </summary>
        public float b { get; set; }

        /// <summary>
        /// Accesses the 'c' element of the matrix.
        /// </summary>
        public float c { get; set; }

        /// <summary>
        /// Accesses the 'd' element of the matrix.
        /// </summary>
        public float d { get; set; }

        /// <summary>
        /// Accesses the 'tx' element of the matrix, which controls translation along the x axis.
        /// </summary>
        public float tx { get; set; }

        /// <summary>
        /// Accesses the 'ty' element of the matrix, which controls translation along the y axis.
        /// </summary>
        public float ty { get; set; }

        /// <summary>
        /// Create a new matrix using the given values. The default values will set the matrix
        /// to the identity transformation.
        /// 
        /// \f$ M_{this} = 
        /// \begin{bmatrix} 
        ///     1  &  0  &  0    \\ 
        ///     0  &  1  &  0    \\ 
        ///     0  &  0  &  1
        /// \end{bmatrix}\f$
        /// </summary>
        public Matrix(float a = 1.0f, float b = 0.0f, float c = 0.0f, float d = 1.0f,
            float tx = 0.0f, float ty = 0.0f)
        {
            this.a = a;
            this.b = b;
            this.c = c;
            this.d = d;
            this.tx = tx;
            this.ty = ty;
        }

        /// <summary>
        /// Create a new matrix by copying the elements of the given matrix.
        /// 
        /// \f$ M_{this} = M_{given} \f$
        /// </summary>
        /// <param name="m">The given matrix to copy.</param>
        public Matrix(Matrix m)
        {
            this.a = m.a;
            this.b = m.b;
            this.c = m.c;
            this.d = m.d;
            this.tx = m.tx;
            this.ty = m.ty;
        }

        /// <summary>
        /// Concatenates the given matrix with this matrix, combining their effects.
        /// Mathematically, this is matrix multiplication:
        /// 
        /// \f$ M_{this} = M_{given} \times M_{this} \f$
        /// </summary>
        /// <param name="m">The given matrix to concatenate.</param>
        public void Concat(Matrix m)
        {
            float a = this.a * m.a + this.b * m.c;
            float b = this.a * m.b + this.b * m.d;
            float c = this.c * m.a + this.d * m.c;
            float d = this.c * m.b + this.d * m.d;
            float tx = this.tx * m.a + this.ty * m.c + m.tx;
            float ty = this.tx * m.b + this.ty * m.d + m.ty;

            this.a = a;
            this.b = b;
            this.c = c;
            this.d = d;
            this.tx = tx;
            this.ty = ty;
        }

        /// <summary>
        /// Copies all of the elements from the given matrix into this matrix.
        /// 
        /// \f$ M_{this} = M_{given} \f$
        /// </summary>
        /// <param name="m">The given matrix to copy.</param>
        public void CopyFrom(Matrix m)
        {
            this.a = m.a;
            this.b = m.b;
            this.c = m.c;
            this.d = m.d;
            this.tx = m.tx;
            this.ty = m.ty;
        }

        /// <summary>
        /// Sets the matrix to the identity transformation. The resulting matrix has
        /// a=1, b=0, c=0, d=1, tx=0, and ty=0.
        /// 
        /// \f$ M_{this} = 
        /// \begin{bmatrix} 
        ///     1  &  0  &  0    \\ 
        ///     0  &  1  &  0    \\ 
        ///     0  &  0  &  1
        /// \end{bmatrix}\f$
        /// </summary>
        public void Identity()
        {
            this.a = 1.0f;
            this.b = 0.0f;
            this.c = 0.0f;
            this.d = 1.0f;
            this.tx = 0.0f;
            this.ty = 0.0f;
        }

        /// <summary>
        /// Sets the matrix to the inverse transformation of the matrix. A matrix
        /// times its inverse is the identity.
        /// 
        /// \f$ M_{this} = {M_{this}}^{-1} \f$
        /// </summary>
        public void Invert()
        {
            float t = this.a * this.d - this.b * this.c;

            float a = (float)(this.d / t);
            float b = (float)(-this.b / t);
            float c = (float)(-this.c / t);
            float d = (float)(this.a / t);
            float tx = (float)((this.c * this.ty - this.d * this.tx) / t);
            float ty = (float)(-(this.a * this.ty - this.b * this.tx) / t);

            this.a = a;
            this.b = b;
            this.c = c;
            this.d = d;
            this.tx = tx;
            this.ty = ty;
        }

        /// <summary>
        /// Apply a rotation transform to the matrix. Mathematically, this concatenates
        /// the matrix with a rotation transform:
        /// 
        /// \f$ M_{this} = 
        /// \begin{bmatrix} 
        ///     cos(angle)  &  -sin(angle) &  0    \\ 
        ///     sin(angle)  &  cos(angle)  &  0    \\ 
        ///     0           &  0           &  1 
        /// \end{bmatrix} \times M_{this} \f$
        /// </summary>
        /// <param name="angle">The angle specified in radians.</param>
        public void Rotate(float angle)
        {
            double sin = global::System.Math.Sin(angle);
            double cos = global::System.Math.Cos(angle);

            float a = (float)(this.a * cos - this.b * sin);
            float b = (float)(this.a * sin + this.b * cos);
            float c = (float)(this.c * cos - this.d * sin);
            float d = (float)(this.c * sin + this.d * cos);
            float tx = (float)(this.tx * cos - this.ty * sin);
            float ty = (float)(this.tx * sin + this.ty * cos);

            this.a = a;
            this.b = b;
            this.c = c;
            this.d = d;
            this.tx = tx;
            this.ty = ty;
        }

        /// <summary>
        /// Apply a scaling transform to the matrix. Mathematically, this concatenates
        /// the matrix with a scaling transform:
        /// 
        /// \f$ M_{this} = 
        /// \begin{bmatrix} 
        ///     s_x  &  0    &  0    \\ 
        ///     0    &  s_y  &  0    \\ 
        ///     0    &  0    &  1
        /// \end{bmatrix} \times M_{this} \f$
        /// </summary>
        /// <param name="sx">Factor to scale along the x axis.</param>
        /// <param name="sy">Factor to scale along the y axis.</param>
        public void Scale(float sx, float sy)
        {
            this.a *= sx;
            this.b *= sy;
            this.c *= sx;
            this.d *= sy;
            this.tx *= sx;
            this.ty *= sy;
        }

        /// <summary>
        /// Sets the elements of the matrix to the given values.
        /// 
        /// \f$ M_{this} = 
        /// \begin{bmatrix} 
        ///     a  &  c  &  t_x    \\ 
        ///     b  &  d  &  t_y    \\ 
        ///     0  &  0  &  1
        /// \end{bmatrix}\f$
        /// </summary>
        public void SetTo(float a, float b, float c, float d, float tx, float ty)
        {
            this.a = a;
            this.b = b;
            this.c = c;
            this.d = d;
            this.tx = tx;
            this.ty = ty;
        }

        /// <summary>
        /// Translate along the x and y axes by the given amounts. Mathematically, this
        /// concatenates the matrix with a translation transform:
        /// 
        /// \f$ M_{this} = 
        /// \begin{bmatrix} 
        ///     1  &  0  &  d_x    \\ 
        ///     0  &  1  &  d_y    \\ 
        ///     0  &  0  &  1
        /// \end{bmatrix} \times M_{this} \f$
        /// </summary>
        /// <param name="dx">Amount to translate along the x axis.</param>
        /// <param name="dy">Amount to translate along the y axis.</param>
        public void Translate(float dx, float dy)
        {
            this.tx += dx;
            this.ty += dy;
        }

        /// <summary>
        /// Returns a formatted string representation of the matrix.
        /// </summary>
        /// <returns>The matrix represented as a string.</returns>
        public override string ToString()
        {
            return string.Format("(a={0}, b={1}, c={2}, d={3}, tx={4}, ty={5})",
                this.a, this.b, this.c, this.d, this.tx, this.ty);
        }

        /// <summary>
        /// Returns a new point by applying the transformation matrix to the given point.
        /// 
        /// \f$ p' = M_{this} \times p \f$
        /// </summary>
        /// <param name="p">The point to transform.</param>
        /// <returns>The transformed point.</returns>
        public Point TransformPoint(Point p)
        {
            float x = (p.x * this.a) + (p.y * this.c) + this.tx;
            float y = (p.x * this.b) + (p.y * this.d) + this.ty;

            return new Point(x, y);
        }

        /// <summary>
        /// An explicit operator to convert a matrix into a Unity Matrix4x4.
        /// </summary>
        public static explicit operator UnityEngine.Matrix4x4(Matrix matrix)
        {
            Matrix4x4 result = Matrix4x4.identity;

            result[0, 0] = matrix.a;
            result[1, 1] = matrix.d;
            result[1, 0] = matrix.b;
            result[0, 1] = matrix.c;
            result[0, 3] = matrix.tx;
            result[1, 3] = matrix.ty;

            return result;
        }
    }
}